package com.iwhalecloud.uncc.service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.annotation.PostConstruct;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.iwhalecloud.uncc.domain.ShellScript;
import com.iwhalecloud.uncc.domain.exception.ScriptExecuteException;
import com.iwhalecloud.uncc.properties.ScriptProperties;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service(value = "javaDynamicScriptExeService")
public class JavaDynamicScriptExeService {

	private static ConcurrentHashMap<String, Class> cache = new ConcurrentHashMap<String, Class>();
	private static ConcurrentHashMap<String, Long> timeStampcache = new ConcurrentHashMap<String, Long>();
	private static ConcurrentHashMap<String, Long> badcache = new ConcurrentHashMap<String, Long>();
	private static ConcurrentHashMap<String, ReentrantLock> lockCache = new ConcurrentHashMap<String, ReentrantLock>();
	// 默认清理两天前的java.class文件
	private static long expireMill = 172800000L;
	// 脚本编译路径
	private String compliePath;
	// jar路径
	private String libPath;
	private ScheduledExecutorService scheduleEcut = null;
	private ExecutorService mainEcut = null;

	@Autowired
	private ScriptProperties scriptProperties;

	@PostConstruct
	public void init() {
		compliePath = (scriptProperties.getRuleEngineerCompliePath() != null
				&& !"".equals(scriptProperties.getRuleEngineerCompliePath()))
						? scriptProperties.getRuleEngineerCompliePath()
						: System.getProperty("java.io.tmpdir");

		libPath = scriptProperties.getRuleEngineerComplieLibPath() == null ? ""
				: scriptProperties.getRuleEngineerComplieLibPath();

		convertLibPath();

		boolean needJavaJar = scriptProperties.isRuleEngineerNeedJavaJar();
		if (needJavaJar) {
			String osName = System.getProperty("os.name");
			String javaHome = System.getProperty("java.home");
			String baseJarPath = javaHome + File.separator + "lib";
			File baseJarFile = new File(baseJarPath);
			if (baseJarFile.exists() && baseJarFile.isDirectory()) {
				File[] files = baseJarFile.listFiles();
				if (files != null) {
					for (File jarFile : files) {
						if (jarFile.isFile() && "rt.jar".equals(jarFile.getName())) {
							if (osName.contains("Windows")) {
								libPath = libPath + ";" + baseJarPath + File.separator + jarFile.getName();
							} else {
								libPath = libPath + ":" + baseJarPath + File.separator + jarFile.getName();
							}
						}
					}
				}
				if (libPath.startsWith(";") || libPath.startsWith(":")) {
					libPath = libPath.substring(1);
				}
			}
		}
		Calendar cal = Calendar.getInstance();
		cal.setTime(new Date());
		long s = cal.getTimeInMillis();
		cal.add(Calendar.DATE, scriptProperties.getRuleEngineerComplieFileExpireTime());
		long e = cal.getTimeInMillis();
		expireMill = e - s;

		mainEcut = Executors.newSingleThreadScheduledExecutor();
		// 定时清理缓存
		scheduleEcut = Executors.newSingleThreadScheduledExecutor();
		scheduleEcut.schedule(new Cleaner(), 86400, TimeUnit.SECONDS);
		// 定时清理文件
		Thread fileCleanerThread = new Thread(new FileCleaner());
		fileCleanerThread.start();
	}

	private void convertLibPath() {
		StringBuffer stringBuffer = new StringBuffer();
		boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
		String markLabel = isWindows ? ";" : ":";
		if (libPath.contains("*")) {
			String[] pathArr = libPath.split(markLabel);
			for (int i = 0; i < pathArr.length; i++) {
				if (pathArr[i].contains("*")) {
					String baseJarPath = pathArr[i].substring(0, pathArr[i].length() - 2);
					File baseJarFile = new File(baseJarPath);
					if (baseJarFile.exists() && baseJarFile.isDirectory()) {
						File[] files = baseJarFile.listFiles();
						if (files != null) {
							for (File jarFile : files) {
								if (jarFile.isFile() && jarFile.getName()
										.substring(jarFile.getName().lastIndexOf(".") + 1).equalsIgnoreCase("jar")) {
									stringBuffer.append(jarFile.getAbsolutePath() + markLabel);
								}
							}
						}
					}
				} else {
					stringBuffer.append(pathArr[i] + markLabel);
				}
			}
			libPath = stringBuffer.toString();
		}
	}

	public Object execute(Object object) throws ScriptExecuteException {
		if (!(object instanceof ShellScript)) {
			throw new IllegalArgumentException("arg[object] is null or not Java Script object!");
		}
		Object result = null;
		ShellScript bss = null;
		try {
			bss = (ShellScript) object;
			long s = System.currentTimeMillis();
			result = run(bss);
			if (log.isDebugEnabled()) {
				log.debug("run script elapse " + (System.currentTimeMillis() - s) + " ms");
				log.debug("run script result=" + result);
			}
		} catch (Exception e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw new ScriptExecuteException("java script[" + bss.getName() + "] is invalid!", e);
		}

		return result;
	}

	// 打包脚本内容
	private String packClassBody(ShellScript bss) {
		String paras = "";
		StringBuffer parasBuffer = new StringBuffer();
		String classBody = "";
		if (bss.getParaMap() != null && bss.getParaMap().size() > 0) {
			Iterator it = bss.getParaMap().keySet().iterator();
			Object key;
			while (it.hasNext()) {
				key = it.next();
				Object value = bss.getParaMap().get(key);
				if (value instanceof String) {
					parasBuffer.append(",String ").append(key);
				} else if (value instanceof List) {
					parasBuffer.append(",List ").append(key);
				} else if (value instanceof Map) {
					parasBuffer.append(",Map ").append(key);
				} else {
					parasBuffer.append(",String ").append(key);
				}
			}
			paras = parasBuffer.toString();
			if (paras.length() != 0) {
				paras = paras.substring(1);
			}
		}
		classBody = "{ public static Object exec(" + paras + ") throws Exception {" + bss.getScript() + "}}";

		return classBody;
	}

	private ReentrantLock getLock(String classBody) throws ScriptExecuteException {
		ReentrantLock tLock = null;
		if (lockCache.containsKey(classBody)) {
			tLock = lockCache.get(classBody);
		} else {
			tLock = new ReentrantLock();
			ReentrantLock rtnLock = lockCache.putIfAbsent(classBody, tLock);
			if (rtnLock == null) {
				log.debug("lockCache.putIfAbsent return null ...");
			}
		}
		return tLock;
	}

	private void releaseLock(String classBody) throws ScriptExecuteException {
		lockCache.remove(classBody);
	}

	private Object run(ShellScript bss) throws ScriptExecuteException {
		if (bss.getScript() == null) {
			throw new IllegalArgumentException("script is null,please choose one!");
		}

		Class clazz = null;
		Object result = null;

		String classBody = packClassBody(bss);

		if (badcache.containsKey(classBody)) {
			throw new IllegalArgumentException("script can not be parsed,please Correct it!");
		}

		try {
			clazz = cache.get(classBody);
			if (clazz == null) {
				Lock l = getLock(classBody);
				l.lock();
				try {
					clazz = cache.get(classBody);
					if (clazz == null) {
						Parser parser = new Parser(classBody);
						Future<Class> future = mainEcut.submit(parser);
						clazz = future.get();
					}
				} catch (Exception e) {
					if (classBody != null) {
						badcache.put(classBody, 1L);
					}
					throw new ScriptExecuteException("script complie error!...", e);
				} finally {
					l.unlock();
					releaseLock(classBody);
				}
			}
			result = run(bss, clazz);
		} catch (Exception e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw new ScriptExecuteException("script complie error!...", e);
		}

		return result;
	}

	private Class cacheClazz(String className, String classBody) throws Exception {
		log.info("<script> url path=[" + "file:" + compliePath + File.separator + "]");
		URL url = new URL("file:" + compliePath + File.separator);
		URLClassLoader urlClass = new URLClassLoader(new URL[] { url });
		Class clazz = urlClass.loadClass(className);
		cache.put(classBody, clazz);

		return clazz;
	}

	private Object run(ShellScript bss, Class clazz) throws Exception {
		Object value = null;
		Class cls = clazz;
		try {
			Map map = bss.getParaMap();
			Class[] argsClass = null;
			Object[] args = null;
			if (map != null && map.size() > 0) {
				argsClass = new Class[bss.getParaMap().size()];
				Iterator it = map.keySet().iterator();
				args = new Object[bss.getParaMap().size()];
				int i = 0;
				Object obj = null;
				while (it.hasNext()) {
					obj = it.next();
					Object av = map.get(obj);
					args[i] = av == null ? "" : av;
					// 判断入参类型
					if (av instanceof String) {
						argsClass[i] = Class.forName("java.lang.String");
					} else if (av instanceof List) {
						argsClass[i] = Class.forName("java.util.List");
					} else if (av instanceof Map) {
						argsClass[i] = Class.forName("java.util.Map");
					} else {
						argsClass[i] = Class.forName("java.lang.String");
					}
					if (log.isDebugEnabled()) {
						log.debug("para-" + obj.toString() + "|value-" + args[i]);
					}
					i++;
				}
			}

			Method main = cls.getMethod("exec", argsClass);

			value = main.invoke(null, args);

		} catch (SecurityException e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw e;
		} catch (NoSuchMethodException e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw e;
		} catch (IllegalArgumentException e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw e;
		} catch (IllegalAccessException e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw e;
		} catch (InvocationTargetException e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw e;
		} catch (ClassNotFoundException e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw e;
		}

		return value;
	}

	class Parser implements Callable<Class> {
		String script;

		public Parser(String script) {
			this.script = script;
		}

		@Override
		public Class call() throws Exception {
			Class clazz = null;
			String className = null;
			String scriptSc = null;

			File file = File.createTempFile("RunTime", ".java", new File(compliePath + File.separator));

			String filename = file.getName();
			className = filename.substring(0, filename.indexOf('.'));
			PrintWriter out = new PrintWriter(new FileOutputStream(file));

			if (libPath != null && libPath.length() > 0) {
				scriptSc = "import java.util.*;\n" + "import java.util.regex.*;\n" + "import com.alibaba.fastjson.*;\n"
						+ "import com.iwhalecloud.uncc.utils.JsonUtil; \n" + " public class " + className;
			} else {
				scriptSc = "public class " + className;
			}
			scriptSc = scriptSc + script;

			if (log.isDebugEnabled()) {
				log.debug("script is :\n" + scriptSc);
			}

			out.println(scriptSc);
			out.flush();
			out.close();
			String[] arg;
			if (libPath == null || libPath.length() == 0) {
				arg = new String[] { "-d", compliePath, compliePath + File.separator + filename };
			} else {
				arg = new String[] { "-cp", libPath, "-d", compliePath, compliePath + File.separator + filename };
			}

//			int status = com.sun.tools.javac.Main.compile(arg);
			JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
			if (javaCompiler == null) {
				throw new Exception("javaCompiler is null, please configure right javaCompiler!");
			}
			int status = javaCompiler.run(null, null, null, arg);

			if (status != 0) {
				boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
				if (isWindows) {
					libPath = scriptProperties.getRuleEngineerComplieLibPath() == null ? ""
							: scriptProperties.getRuleEngineerComplieLibPath();
				}
				String compile = " javac -cp " + libPath + " " + compliePath + File.separator + filename;
				String command = compile;
				try {
					Process process = Runtime.getRuntime().exec(command);
					getErrorMessage(process);
				} catch (IOException e) {
					log.error(ExceptionUtils.getStackTrace(e));
				}
				throw new ScriptExecuteException("script complie error!...");
			}
			if (!scriptProperties.isTestMode()) {
				file.delete();
			}

			clazz = cacheClazz(className, script);
			timeStampcache.put(script, System.currentTimeMillis());

			return clazz;
		}

		private void getErrorMessage(Process process) {
			String newLine = System.getProperty("line.separator");
			try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
				String line = errorReader.readLine();
				StringBuffer sb = new StringBuffer();
				if (line != null) {
					sb.append(line).append(newLine);
					while ((line = errorReader.readLine()) != null) {
						sb.append(line).append(newLine);
					}
				}
				if (sb.length() > 0) {
					log.error(sb.toString());
				}
			} catch (IOException e) {
				log.error(ExceptionUtils.getStackTrace(e));
			}
		}
	}

	class Cleaner implements Runnable {
		@Override
		public void run() {
			log.info("cleaning cache script...");
			badcache.clear();
			Long current = System.currentTimeMillis();
			Set keys = timeStampcache.keySet();
			Iterator it = keys.iterator();
			while (it.hasNext()) {
				Object key = it.next();
				Long time = timeStampcache.get(key);
				if (current - time >= 1000 * 3600 * 24 * 7) {
					log.info("clean cache script...");
					cache.remove(key);
					timeStampcache.remove(key);
				}
			}
		}
	}

	class FileCleaner implements Runnable {
		@Override
		public void run() {
			while (true) {
				try {
					log.info("delete java dynamic rule expired .java & .class file ...");
					File file = new File(compliePath);
					File[] javaFiles = file.listFiles();
					if (javaFiles != null) {
						for (File javaFie : javaFiles) {
							try {
								String fileName = javaFie.getName();
								long mt = javaFie.lastModified();
								if (javaFie.isFile()
										&& (fileName.toLowerCase().endsWith(".java")
												|| fileName.toLowerCase().endsWith(".class"))
										&& (System.currentTimeMillis() - mt > expireMill)) {
									boolean delete = javaFie.delete();
									if (!delete) {
										log.info("file delete failed:" + fileName);
									}
									if (log.isInfoEnabled()) {
										log.info("delete java dynamic rule expired .java .class tmp file:" + fileName);
									}
								}
							} catch (Exception e) {
								log.error(ExceptionUtils.getStackTrace(e));
							}
						}
					}
				} catch (Exception e) {
					log.error(ExceptionUtils.getStackTrace(e));
				} finally {
					try {
						// 一天清理一次
						Thread.sleep(86400000L);
					} catch (Exception e) {
						log.error(ExceptionUtils.getStackTrace(e));
					}
				}
			}
		}
	}
}
